Skip to content

gh-122622: Add PyREPL simple_interact.statement_submitted_hook - #157241

Open
edvilme wants to merge 14 commits into
python:mainfrom
edvilme:pyrepl-hook
Open

edvilme wants to merge 14 commits into
python:mainfrom
edvilme:pyrepl-hook

Conversation

@edvilme

@edvilme edvilme commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Supersedes #144724

Adds a statement_submitted_hook hook to _pyrepl.simple_interact that gets triggered after the user has submitted a statement (not necessarily a line) but before it is executed.

Rationale

This should be configurable by external tools such as IDEs who may want to perform an action before a statement is executed, such as displaying custom escape sequences for terminal integration, handling custom logging logic, performing analysis, etc.
This hook should allow said tools to perform an action.

Example (Timer)

>>> import _pyrepl
>>> import sys
>>> import time
>>> class PS1:
...     start_time = None
...     def reset_start_time(self):
...         self.start_time = time.perf_counter()
...     def __init__(self):
...         _pyrepl.simple_interact.statement_submitted_hook = lambda x: self.reset_start_time()
...     def __str__(self):
...         end_time = time.perf_counter()
...         time_delta = end_time - self.start_time
...         return f"# Execution took {time_delta}s \n>>> "
...
>>> sys.ps1 = PS1()
>>> 1 + 1 
2
# Execution took 0.0021207999670878053s
>>> [i for i in range(100)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
# Execution took 0.003159299958497286s

Example (VSCode)

VS Code uses custom escape sequences to indicate when a command execution has started and finished. Here, the hook could be used to print an invisible escape sequence to the terminal. (Note that the example uses ### EXEC ### to represent it visually, but escape sequences are invisible)

image

With error reporting

image

AI Usage

AI was used to help with merge conflicts

@brettcannon
brettcannon self-requested a review September 17, 2026 21:15
Comment thread Lib/_pyrepl/__init__.py Resolved
Comment thread Lib/_pyrepl/simple_interact.py Resolved
Comment thread Lib/_pyrepl/simple_interact.py Resolved
Comment thread Lib/_pyrepl/simple_interact.py Resolved
Comment thread Lib/_pyrepl/simple_interact.py Resolved
@bedevere-app

bedevere-app Bot commented Sep 17, 2026

Copy link
Copy Markdown

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

Comment thread Lib/_pyrepl/simple_interact.py Resolved
@edvilme edvilme changed the title gh-122622: Add PyREPL pre_execution_hook gh-122622: Add PyREPL simple_interact.statement_submitted_hook Sep 19, 2026
@edvilme

edvilme commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

I have made the requested changes; please review again

@bedevere-app

bedevere-app Bot commented Sep 19, 2026

Copy link
Copy Markdown

Thanks for making the requested changes!

@brettcannon: please review the changes made to this pull request.

@bedevere-app
bedevere-app Bot requested a review from brettcannon September 19, 2026 01:20
@brettcannon brettcannon added type-bug An unexpected behavior, bug, or error needs backport to 3.14 bugs and security fixes needs backport to 3.15 bugs and security fixes labels Oct 1, 2026
@brettcannon brettcannon added the topic-repl Related to the interactive shell label Oct 1, 2026
@bedevere-app bedevere-app Bot added the type-feature A feature request or enhancement label Oct 13, 2026
edvilme and others added 2 commits October 13, 2026 14:51
Preserve both ANY and MagicMock when merging the interactive console tests, keeping upstream prompt coverage and statement-submitted hook coverage intact.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 51fc76fa-b241-49aa-830c-840d2d812b54
@brettcannon

Copy link
Copy Markdown
Member

@pablogsal @ambv feel free to review, but if you're too busy I will just plan to merge in about a week.

@johnslavik johnslavik left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is (primarily) a _pyrepl convenience for VS Code, I'd try to make this PR smaller.

Comment on lines +413 to +418
def test_hook_return_value_is_ignored(self):
hook = MagicMock(return_value=True)

_, namespace = self._run_interactive(["x = 1"], hook)

self.assertEqual(namespace["x"], 1)

@johnslavik johnslavik Oct 31, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure there's a reliable way to test that the return value is ignored. I'd remove this case from being tested.

patch.object(simple_interact, "append_history_file"),
patch("_pyrepl.readline._setup"),
contextlib.redirect_stdout(output),
contextlib.redirect_stderr(output),

@johnslavik johnslavik Oct 31, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't rely on mocks of internal attributes.

I'd test this with a PTY instead, maybe in test_pyrepl.TestMain?
That ensures the things that need to be displayed to the user are actually displayed to the user.

Example:

def test_showrefcount(self):
env = os.environ.copy()
env.pop("PYTHON_BASIC_REPL", "")
output, _ = self.run_repl("1\n1+2\nexit()\n", cmdline_args=['-Xshowrefcount'], env=env)
matches = re.findall(r'\[-?\d+ refs, \d+ blocks\]', output)
self.assertEqual(len(matches), 3)
env["PYTHON_BASIC_REPL"] = "1"
output, _ = self.run_repl("1\n1+2\nexit()\n", cmdline_args=['-Xshowrefcount'], env=env)
matches = re.findall(r'\[-?\d+ refs, \d+ blocks\]', output)
self.assertEqual(len(matches), 3)

(This applies to all tests in this PR, I'd try to fit all of them into a few tests in test_pyrepl.TestMain)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A PTY test makes sense here. The unit tests still exercise real statement execution, so I see no need to replace all of them.

One detail: the example needs flush=True, or the execution marker can stay in the buffer until the statement finishes. Our PTY helper uses -u, so it will miss that case.

try:
statement_submitted_hook(statement)
except Exception:
console.showtraceback()

@johnslavik johnslavik Oct 31, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's now easy to misread a traceback from the hook's exception as an exception that comes from running the user code:

>>> from functools import singledispatch
Traceback (most recent call last):
  File "<python-input-7>", line 2, in hook
    1/0
    ~^~
ZeroDivisionError: division by zero
>>>

(I've set the hook in the session, but even if there's internal VSCode paths included in the traceback that are telling this is some hook hackery, I still think this could be improved.)

Additionally, that traceback is displayed before the user code exception traceback.

I'd prefer:

  1. Reporting the full traceback to reader.debug() (a REPL hacker can then uncomment debug() code for verbose error reporting)
  2. Reporting that an "internal" error happened to reader.error() error overlay (something like "exception occurred while running an internal prompt hook")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the traceback needs a clear hook label, but I prefer to keep it visible. reader.debug() currently does nothing, so that loses useful details.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a behavior issue with console.showtraceback() here. It replaces sys.last_exc and the other sys.last_* attributes. If the hook fails on each submission, pdb.pm() sees the hook error instead of the previous user exception. I think hook error reports need to preserve that state. This also needs a regression test.

# In the REPL:
# >>> print("Hello, world!")
# \x1b]633;C\x07Hello, world!
statement_submitted_hook = None

@johnslavik johnslavik Oct 31, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the explicit description of the intent!
OTOH, this is a private API for (mainly) the benefit of VS Code. I'd make this shorter to reduce maintenance costs:

  • I'd remove the example entirely, people hacking the REPL know how to set a module attribute.
  • The only comment I'd leave is # not called for commands, return value is ignored
    I wouldn't pay much attention to exception handling, the default convention is to report all errors somehow. What would be more concerning is if the errors weren't displayed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can shorten this and remove the example, but keep the callback contract. External tools need to know when it runs, what it receives, and how errors behave.

“Submitted input” is more accurate than “complete statement”. Syntax errors and blank input reach the hook too. The exception description also needs to distinguish Exception from KeyboardInterrupt and SystemExit.

from .readline import _get_reader, multiline_input, append_history_file


# Called with the complete statement, which may span multiple lines, after the

@johnslavik johnslavik Oct 31, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, one more thing.

This isn't exactly correct. I think a b c is not a "complete statement", no?

>>> import _pyrepl.simple_interact as si
>>> si.statement_submitted_hook = lambda x: print("hook called")
>>> a b c
hook called
  File "<python-input-19>", line 1
    a b c
      ^
SyntaxError: invalid syntax

Perhaps we could think if there's a better name?


if statement_submitted_hook is not None:
try:
statement_submitted_hook(statement)

@johnslavik johnslavik Oct 31, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think it would make sense to heat-treat statement with _strip_final_ident before passing it over.

What would you rather receive on the hook in the case below, - or +?

 >>> def f():
 ...     'there will be some final indent'
 ...
-hook called with "def f():\n    'there will be some final indent'\n    "
+hook called with "def f():\n    'there will be some final indent'\n"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting merge needs backport to 3.14 bugs and security fixes needs backport to 3.15 bugs and security fixes topic-repl Related to the interactive shell type-bug An unexpected behavior, bug, or error type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants